package com.svconnect.serialization;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import com.svconnect.annotations.DataContract;
import com.svconnect.annotations.DataMember;
import com.svconnect.annotations.Dictionary;
import com.svconnect.exceptions.CircularDependencyException;
import com.svconnect.exceptions.NotNullableException;
import com.svconnect.services.DictInvocationParam;
import com.svconnect.services.InvocationParam;
import com.svconnect.util.Log;
import com.svconnect.util.StringUtil;

public class XMLSerializer {

	private static final Class<?>[] simpleTypes = new Class<?>[] {
			Boolean.class, Byte.class, Date.class, Double.class, Float.class,
			Integer.class, Long.class, Short.class, String.class,
			Character.class, UUID.class,
			boolean.class, byte.class, double.class, float.class,
			int.class, long.class, short.class, char.class };
	
	private static final String NULL_NS = "i";

	private int namespaceNo = 4;
	private boolean containsNulls = false;
	private Document document;

	private String rootNamespace;
	
	private Map<Class<?>, String> dataMemberNamespaces;
	private Map<Class<?>, String> dictKeyValTypeNames;
	
	private boolean isInitialized = false;
	private boolean nullableHandled = true;

	public XMLSerializer() {}
	public XMLSerializer(String rootNamespace, Document document) {
		this.rootNamespace = rootNamespace;
		this.document = document;
	}
	
	public boolean isNullableHandled() {
		return nullableHandled;
	}
	public void setNullableHandled(boolean nullableHandled) {
		this.nullableHandled = nullableHandled;
	}	
	
	public void init() {
		if (!isInitialized) {
			namespaceNo = 4;
			containsNulls = false;
			dataMemberNamespaces = new HashMap<Class<?>, String>();
			dictKeyValTypeNames = new HashMap<Class<?>, String>();
			isInitialized = true;
		}
	}	
	
	public String getRootNamespace() {
		return rootNamespace;
	}

	public void setRootNamespace(String rootNamespace) {
		this.rootNamespace = rootNamespace;
	}	
	
	public Document getDocument() {
		return document;
	}
	public void setDocument(Document document) {
		this.document = document;
	}	
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Object readObject(Type aType, Element element) throws ParseException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {

		if (isNull(element))
			return null;

		Class<?> type = null;
		if (aType instanceof ParameterizedType) {
			type = (Class<?>)((ParameterizedType) aType).getRawType();
		}
		else {
			type = (Class<?>)aType;
		}
		
		String value = element.getTextContent();
		if (type.equals(void.class) || type.equals(Void.class))
			return null;
		else if (type.equals(boolean.class) || type.equals(Boolean.class))
			return Boolean.parseBoolean(value);
		else if (type.equals(byte.class) || type.equals(Byte.class)) 
			return Byte.parseByte(value);
		else if (type.equals(double.class) || type.equals(Double.class))
			return Double.parseDouble(value);
		else if (type.equals(float.class) || type.equals(Float.class)) 
			return Float.parseFloat(value);
		else if (type.equals(int.class) || type.equals(Integer.class))
			return Integer.parseInt(value);
		else if (type.equals(long.class) || type.equals(Long.class))
			return Long.parseLong(value);
		else if (type.equals(short.class) || type.equals(Short.class))
			return Short.parseShort(value);
		else if (type.equals(char.class) || type.equals(Character.class))
			return value.charAt(0);
		else if (type.equals(String.class))
			return value;
		else if (type.equals(UUID.class))
			return UUID.fromString(value);
		else if (type.equals(Date.class)) {
			
			value = value.replaceFirst("\\.\\d+", "");
			int colonIdx = value.lastIndexOf(':');
			if (colonIdx>19)
				value = value.substring(0, colonIdx)+value.substring(colonIdx+1);
			else
				value += "+0000";
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ssZ");
			return sdf.parse(value);
		}
		else if (type.equals(Map.class)) {
			if (aType instanceof ParameterizedType) {
			    ParameterizedType paramType = (ParameterizedType)aType;
			    Type[] fieldArgTypes = paramType.getActualTypeArguments();
			    if (fieldArgTypes.length==2) {
			    	Type keyType = fieldArgTypes[0];
			    	Type valType = fieldArgTypes[1];
			    	
			    	Map map = new HashMap();
			    	NodeList children = element.getChildNodes();
			    	for (int i=0; i<children.getLength(); i++) {
			    		Element child = (Element)children.item(i);
			    		Element keyElem = this.getChildElementByFieldName(child, "Key");
			    		Object key = this.readObject(keyType, keyElem);
			    		Element valElem = this.getChildElementByFieldName(child, "Value");
			    		Object val = this.readObject(valType, valElem);
			    		map.put(key, val);
			    	}
			    	return map;
			    }
			}
			return null;
		}
		else if (type.equals(List.class)) {
			if (aType instanceof ParameterizedType) {
			    ParameterizedType paramType = (ParameterizedType)aType;
			    Type[] fieldArgTypes = paramType.getActualTypeArguments();
			    if (fieldArgTypes.length==1) {
			    	Type itemType = fieldArgTypes[0];
			    	
			    	List list = new ArrayList();
			    	NodeList children = element.getChildNodes();
			    	for (int i=0; i<children.getLength(); i++) {
			    		Element child = (Element)children.item(i);
			    		Object item = this.readObject(itemType, child);
			    		list.add(item);
			    	}
			    	return list;
			    }
			}
		return null;
		}
		else if (type.isEnum()) {
			Method valueOfMeth = type.getMethod("valueOf", new Class<?>[] { String.class });
			return valueOfMeth.invoke(null, value);
		}
		else {
			Object obj = type.newInstance();
			Field[] fields = type.getDeclaredFields();
			for (Field f : fields) {
				DataMember dataMember = f.getAnnotation(DataMember.class);
				if (dataMember==null)
					continue;
				
				Element elem = this.getChildElementByFieldName(element, dataMember.name());
				if (elem==null) {
					throw new ParseException("Field "+f.getName()+" not found",0);
				}
				
				Object innerVal = this.readObject(f.getGenericType(), 
						elem);
				
				if (f.getModifiers() == Field.PUBLIC) {
					f.set(obj, innerVal);
				}
				else {
					String setterName = String.format("set%s",StringUtil.toUpperCaseFirst(f.getName()));
					Method setter = type.getMethod(setterName, new Class<?>[] { f.getType() });
					setter.invoke(obj, innerVal);
				}
			}
			return obj;
		}
	}
	
	private Element getChildElementByFieldName(Element elem, String name) {
		if (elem!=null) {
			NodeList childNodes = elem.getChildNodes();
			for(int i=0; i<childNodes.getLength(); i++) {
				Element childElem = (Element)childNodes.item(i);
				if (childElem.getNodeName().endsWith(name)) {
					return childElem;
				}
			}
		}
		return null;
	}
	
	private boolean isNull(Element elem) {
		if (elem==null)
			return true;
		NamedNodeMap nodeMap = elem.getAttributes();
		for(int i=0; i<nodeMap.getLength(); i++) {
			Attr attr = (Attr)nodeMap.item(i);
			if (attr.getName().endsWith(":nil")) {
				return Boolean.parseBoolean(attr.getValue());
			}
		}
		return false;
	}
	
	
	public Element writeObject(InvocationParam p) {
		this.init();
		if (p instanceof DictInvocationParam) {
			this.dictKeyValTypeNames.put(p.getType(), ((DictInvocationParam)p).getKeyValuePairTypeName());
		}
		return this.writeObject(p.getType(), p.getName(), p.getValue());
	}

	public Element writeObject(Class<?> type, String name, Object value) {
		try {
			this.init();
			Map<String,String> definedNS = new HashMap<String,String>();
			definedNS.put(this.getRootNamespace(),"");
			Element rootElement = writeObject(type, name, value, this.getRootNamespace(), definedNS, new ArrayList<Object>());
			if (this.containsNulls) {
				rootElement.setAttribute("xmlns:"+NULL_NS, "http://www.w3.org/2001/XMLSchema-instance");
			}
			isInitialized = false;
			return rootElement;
		}
		catch(RuntimeException ex) {
			isInitialized = false;
			throw ex;
		}
		catch(Exception ex) {
			Log.e(ex);
			isInitialized = false;
			return null;
		}
	}

	private Element writeObject(Class<?> type, String name, Object value, String namespace, Map<String,String> definedNS, List<Object> definedObjs) 
			throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, SecurityException, InvocationTargetException {
		
		if (definedObjs.contains(value)) throw new CircularDependencyException(value);
		definedObjs.add(value);
		
		Element element = null;
		if (isSimpleType(type)) {
			String namespaceId = definedNS.get(namespace);
			String elemName = (namespaceId==null || namespaceId.isEmpty()) ? name : String.format("%s:%s", namespaceId, name);
			element = document.createElement(elemName);
			if (value==null) {
				this.containsNulls = true;
				element.setAttribute(NULL_NS+":nil", "true");
			}
			else {
				String val = value.toString();
				if (type.equals(Date.class)) {
					Calendar cal = Calendar.getInstance();
					cal.setTime((Date)value);
					SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss.SZ");
					val = sdf.format((Date)value);
					val = String.format("%s:%s",val.substring(0, val.length()-2),val.substring(val.length()-2));
					
				}
				element.setTextContent(val);
			}
		}
		else {

			String namespaceId = definedNS.get(namespace);
			if (namespaceId != null) {
				if (namespaceId.isEmpty())
					element = document.createElement(name);
				else
					element = document.createElement(namespaceId+":"+name);
			}
			else {
				element = document.createElement(name);
				element.setAttribute("xmlns", namespace);
			}
			
			String ns = this.getNamespace(type);
			if (ns!=null && !definedNS.containsKey(ns)) {
				String nId = this.getNextNamespaceID();
				definedNS.put(ns, nId);
				element.setAttribute("xmlns:"+nId, ns);
			}
			
				if (value!=null) {
					if (type == Map.class) {
						
						String nsId = definedNS.get(ns);
						String tagName = dictKeyValTypeNames.get(type);
						if (nsId!=null && !nsId.isEmpty()) {
							tagName = nsId+":"+tagName;
						}
						
						Map<?, ?> m = (Map<?, ?>)value;
						Object[] keyArr = m.keySet().toArray();
						for(Object key : keyArr) {
							Element keyValPairElem = document.createElement(tagName);
							Object val = m.get(key);
							Element keyElem = this.writeObject(key.getClass(), "Key", key, ns, copyMap(definedNS), this.copyList(definedObjs));
							Element valElem = this.writeObject(val.getClass(), "Value", val, ns, copyMap(definedNS), this.copyList(definedObjs));
							keyValPairElem.appendChild(keyElem);
							keyValPairElem.appendChild(valElem);
							element.appendChild(keyValPairElem);
						}
					}
					else if (type == List.class) {
						List<?> objList = (List<?>)value;
						
						if (objList.size()>0) {
							Object firstObj = objList.get(0);
							String innerNs = this.getNamespace(firstObj.getClass());
							if (innerNs!=null && !definedNS.containsKey(innerNs)) {
								String nId = this.getNextNamespaceID();
								definedNS.put(innerNs, nId);
								element.setAttribute("xmlns:"+nId, innerNs);
							}
							for(Object obj : objList) {
								String clsName = obj.getClass().getSimpleName();
									
								Element objElem = this.writeObject(obj.getClass(), clsName, obj, innerNs, copyMap(definedNS), this.copyList(definedObjs));
								element.appendChild(objElem);
							}
						}
						
					}
					else if (type.isEnum()) {
						element.setTextContent(value.toString());
					}
					else {
						boolean first = true;
						do {
							if (!first) {
								ns = this.getNamespace(type);
							}
							
							Field[] fields = type.getDeclaredFields();
							for(Field f : fields) {
								DataMember dataMember = f.getAnnotation(DataMember.class);
								if (dataMember==null)
									continue;
								
								Dictionary dictionaryAddn = f.getAnnotation(Dictionary.class);
								if (dictionaryAddn!=null) {
									this.dictKeyValTypeNames.put(f.getType(), dictionaryAddn.value());
								}
								
								Object val = null;
								if (f.getModifiers() == Field.PUBLIC) {
									val = f.get(value);
								}
								else {
									String getterName = String.format("get%s",StringUtil.toUpperCaseFirst(f.getName()));
									Method getter = type.getMethod(getterName, new Class<?>[] {});
									val = getter.invoke(value);
								}
								
								if (!dataMember.namespace().isEmpty()) {
									if (!definedNS.containsKey(ns))
										definedNS.remove(ns);
									dataMemberNamespaces.put(f.getType(), dataMember.namespace());
								}
								
								if (nullableHandled && !dataMember.nullable() &&val==null)
									throw new NotNullableException(f.getType());
								
								Element innerElem = this.writeObject(f.getType(), dataMember.name(), val, ns, copyMap(definedNS), this.copyList(definedObjs));
								element.appendChild(innerElem);
							}
							type = type.getSuperclass();
							first = false;
						}
						while(type != null && !type.equals(Object.class));
					}
				}
				else {
					this.containsNulls = true;
					element.setAttribute(NULL_NS+":nil", "true");
				}
			
		}
		
		return element;
	}
	
	
	private Map<String,String> copyMap(Map<String,String> src) {
		Map<String,String> dst = null;
		if (src!=null) {
			dst = new HashMap<String,String>();
			for(String key : src.keySet()) {
				dst.put(key, src.get(key));
			}
		}
		return dst;
	}
	
	private List<Object> copyList(List<Object> src) {
		List<Object> objs = null;
		if (src!=null) {
			objs = new ArrayList<Object>();
			for(Object o : src) {
				objs.add(o);
			}
		}
		return objs;
	}
	
	public String getNamespace(Class<?> type) {
		DataContract dataContract = type.getAnnotation(DataContract.class);
		if (dataContract!=null) {
			return dataContract.namespace();
		}
		if (dataMemberNamespaces.containsKey(type)) {
			return dataMemberNamespaces.get(type);
		}
		return null;
	}

	private String getNextNamespaceID() {
		return String.format("d%dp1", namespaceNo++);
	}

	private boolean isSimpleType(Class<?> type) {
		for (Class<?> cls : simpleTypes) {
			if (type.equals(cls))
				return true;
		}
		return false;
	}
}
